/*c99中变长数组
允许我们可以使⽤变量指定数组⼤⼩。*/

/*#include <stdio.h>

int main()
{
    int n = 0;
    scanf("%d", &n);
    int arr[n];//不能初始化，否则报错，也就是不能={0}...
    // 输入
    int i = 0;
    for (i = 0; i < n; i++)
    {
        scanf("%d", &arr[i]); // 还可以arr[i]=i,比如输入100，自动生成1~99.
    }
    // 输出
    for (i = 0; i < n; i++)
    {
        printf("%d ", arr[i]);
    }

    return 0;
}*/

/*字符和ASCII编码
char ,这种类型专⻔是⽤来创建字符变量，字符变量是⽤来存储字符的，
所谓字符就是我们在键盘上敲出来的单个符号，如： A,a,1,@,#,$,~ 等等，C语⾔规定，字符必须要放在单引号中。*/

/*打印a
#include <stdio.h>

int main()
{
    char ch = 'a';
    printf("%c\n", ch);
    printf("%c\n", 97);
    printf("%c\n", 'a');

    return 0;
}*/

/*打印32~127的ASCii代表值
#include <stdio.h>

int main()
{
    for (int i = 32; i <= 127; i++)
    {
        printf("%c ", i);
        if (i % 15 == 0) // 打印15个就换行
            printf("\n");
    }

    return 0;
}*/

/*转义字符
 在字符中有⼀组特殊的字符是转义字符，转义字符顾名思义：转变原来的意思。
 ⽐如：我们有字符 n ，在字符串中打印的时候⾃然能打印出这个字符，如下：*/

/*#include <stdio.h>

int main()
{
    printf("abc\ndef");

    return 0;
}*/

/*转义字符还有⼀些，具体如下：
• \? ：在书写连续多个问号时使⽤，防⽌他们被解析成三字⺟词，在新的编译器上没法验证了。
• \' ：⽤于表⽰字符常量'
• \" ：⽤于表⽰⼀个字符串内部的双引号
• \\ ：⽤于表⽰⼀个反斜杠，防⽌它被解释为⼀个转义序列符。
• \a ：警报，这会使得终端发出警报声或出现闪烁，或者两者同时发⽣。
• \b ：退格键，光标回退⼀个字符，但不删除字符。
• \f ：换⻚符，光标移到下⼀⻚。在现代系统上，这已经反映不出来了，⾏为改成类似于 \v 。
• \n ：换⾏符。
• \r ：回⻋符，光标移到同⼀⾏的开头。
• \t ：制表符，光标移到下⼀个⽔平制表位，通常是下⼀个8的倍数。• \v ：垂直分隔符，光标移到下⼀个垂直制表位，通常是下⼀⾏的同⼀列。
两类特殊的转义字符
下⾯2种转义字符可以理解为：字符的8进制或者16进制表⽰形式
• \ddd ：d d d表⽰1~3个⼋进制的数字。 如： \130 表⽰字符X
8进制转义序列拥有三个⼋进制位的⻓度限制，但若提前遇到不是合法⼋进制位的字符，则在⾸个这种
字符处终⽌。
• \xdd ：d d表⽰2个⼗六进制数字。 如： \x30 表⽰字符016进制转义序列⽆⻓度限制，并在⾸个不是合法⼗六进制位的字符处终⽌。
\0 ：null 字符，代表没有内容， \0 就是 \ddd 这类转义字符的⼀种，⽤于字符串的结束标志，其ASCII码值是0*/

/*#include <stdio.h>

int main()
{
    printf("%c\n", '\'');
    printf("%s\n", "\"");
    printf("c:\\test\\code\\test.c\n");
    printf("\a");
    printf("%c\n", '\130'); // 130是8进制，转换成10进制是88，以88作为ASCII码值的字符是 'X'
    printf("%c\n", '\x30'); // x30中的30是16进制，转换成10进制是48，以48作为ASCII码值的字符是 '0'

    return 0;
}*/

/*字符串和字符数组
 C语⾔中有字符类型，但是没有字符串类型，C语⾔中 字符串 就是由双引号引起来的⼀串字符，⽐
如："abcdef";
⼀个字符串中我们直观的能看到⼀些字符，⽐如：字符串常量 "abcdef" 中，我们看到了 a、b、c、
d、e、f 这6个字符，但是实际上在末尾还隐藏⼀个 '\0' 的转义字符， '\0' 是作为字符串的结束标志存在的。
正因为字符串中隐藏⼀个'\0'字符，是字符串的结束标志，所以我们在使⽤库函数打印字符串
（printf）或者计算字符串⻓度(strlen) 的时候，遇到 '\0' 的时候就⾃动停⽌了。
其实字符串和字符数组是⾮常类似的，字符串在内存中存储的时候，也是连续存放的，就像数组⼀样。*/
/*字符数组的创建和初始化字符数组就⼀个存放字符的数组，创建形式如下：*/

/*#include <stdio.h>

int main()
{
    //"abcdef";//常量字符串
    // char 类型的数组中

    char arr1[5] = {'a', 'b', 'c', 'd', 'e'}; // 初始化
 //=char arr1[5] = "abcde"; //字符串常量初始化字符数组有个\0
    //             0   1   2   3   4
    int sz = sizeof(arr1) / sizeof(arr1[0]);
    int i = 0;
    for (i = 0; i < sz; i++)
    {
        printf("%c", arr1[i]);
    }

    return 0;
}*/

/*\0的重要性*/
/*#include <stdio.h>

int main()
{
    char arr1[] = {'a', 'b', 'c'}; // 可以加个‘\0'后面就没有乱码
    char arr2[] = "abc";

    printf("%s\n", arr1); // 后面有乱码，后面没有\0
    printf("%s\n", arr2);

    return 0;
}*/

/*字符数组的输⼊和输出
我们可以使⽤scanf函数和printf函数完成字符串的输⼊和输出*/
/*#include <stdio.h>

int main()
{
    char arr[10] = {0};
    // 输入操作
    scanf("%s", arr);
    //注：使⽤scanf函数输⼊的时候，我们⾃⼰要保证字符数组⾜够⼤，能够容纳下输⼊进去的字符
    //，要不然就会出问题。这也是scanf被诟病不安全的地⽅。
    // 输出操作
    printf("%s", arr);

    return 0;
}*/

/*求字符串⻓度
在C语⾔中有⼀个库函数叫 strlen ，这个函数是专⻔⽤来求字符串⻓度的。
 strlen 的使⽤需要包含⼀个头⽂件 string.h 。
strlen函数统计的是字符串中 \0 之前的字符的个数，
所以传递给strlen函数的字符串中必须得包含\0 .*/

/*#include <stdio.h>
#include <string.h>

int main()
{
    char str1[10] = "abcdef";
    printf("%zd\n", strlen(str1)); // 6

    char str2[10] = {'a', 'b', 'c', 'd', 'e', 'f'};
    printf("%zd\n", strlen(str2)); // 6

    char str3[] = {'a', 'b', 'c', 'd', 'e', 'f'}; // 可以主动放个'\0'
    printf("%zd\n", strlen(str3));                // 随机值

    // strlen和sizeof的对⽐
    printf("%d\n", strlen(str1));
    printf("%d\n", sizeof(str1));

    return 0;
}*/

/*gets 和 puts 函数*/

/*gets*/
/*#include <stdio.h>

int main()
{
    char arr[20] = {0};
    gets(arr); // 约等于scanf("%s", &arr);
    printf("%s\n", arr);

    return 0;
}*/

/*puts
puts函数打印str指向的字符串到标准输出（⼀般指屏幕），
同时在打印结束后会打印换⾏。*/

/*#include <stdio.h>
int main()
{
    char arr1[] = "abc";
    char arr2[] = "def";
    printf("%s", arr1); // abc
    printf("%s", arr2); // def
    printf("\n");       // 区分两次打印的效果
    puts(arr1);         // abc\n
    puts(arr2);         // def\n

    return 0;
}*/

/*⾃定义函数*/

/*函数的语法形式
其实⾃定义函数和库函数是⼀样的，形式如下：
ret_type fun_name(形式参数)
{

}
1
2
3
4
• ret_type 是函数返回类型
• fun_name 是函数名
• 括号中放的是形式参数
• {} 括起来的是函数体*/

/*#include <stdio.h>
int main()
{
    int a = 0;
    int b = 0;
    // 输⼊
    scanf("%d %d", &a, &b);
    // 调⽤加法函数，完成a和b的相加//求和的结果放在r中
    //to do
//输出
    printf("%d\n", r);
    return 0;
}*/
/*给函数取名：Add，函数Add需要接收2个整型类型的参数，函数计算的结果也是整型。*/
/*形参是实参的一份临时拷贝*/

/*#include <stdio.h>
int Add(int x, int y)//x，y是形参
{
    int z = 0;//可以简化成retrun x+y;
    z = x + y;
    return z;
}

int main()
{
    int a = 0;
    int b = 0;
    scanf("%d %d", &a, &b);
    int r = Add(a, b);//实参
    printf("%d\n", r);

    return 0;
}*/

/*判断闰年*/
/*#include <stdio.h>
int is_leap_year(int y)
{
    if (y % 4 == 0 && y % 100 != 0 || y % 400 == 0)
        return 1;
    else
        return 0;
}

int main()
{
    int year = 0;
    scanf("%d", &year);
    int ret = is_leap_year(year);//函数调用
    if (ret == 1)
        printf("Yes\n");
    else
        printf("No\n");

    return 0;
}*/

/*⾃定义函数的设计
函数的参数
函数返回值类型
函数的功能
函数的名字*/

/*函数的链式访问
所谓链式访问就是将⼀个函数的返回值作为另外⼀个函数的参数，
像链条⼀样将函数串起来就是函数的链式访问。*/

/*#include <stdio.h>
#include <string.h>

int main()
{
    // size_t len = strlen("abcdef"); // 求字符串的长度a,b,c,d,e,f,\0
    // printf("%zd\n", len);

    printf("%zd\n", strlen("abcdef"));

    return 0;
}*/

/*!!printf特殊应用#include <stdio.h>

int main()
{
    printf("%d", printf("%d", printf("%d", 43)));    // 4321
    printf("%d ", printf("%d ", printf("%d ", 43))); // 43 3 2

    return 0;
}*/

/*函数的镶嵌套用
不能嵌套定义*/
/*？!!!!!计算某年某⽉有多少天？
如果要函数实现，可以设计2个函数:
• is_leap_year()：根据年份确定是否是闰年
• get_days_of_month()：调⽤is_leap_year确定是否是闰年后，再根据⽉计算这个⽉的天数*/

/*#include <stdio.h>

int is_leap_year(int y)
{
    if (y % 4 == 0 && y % 100 != 0 || y % 400 == 0)
        return 0;
    else
        return 1;
}

int get_days_of_month(int y, int m)
{
    int days[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    //            0   1   2   3   4   5  对齐下标
    int d = days[m];
    if (is_leap_year(y) == 1 && m == 2)
        d += 1;
    return d;
}

int main()
{
    int y, m;
    scanf("%d%d", &y, &m);
    int d = get_days_of_month(y, m);
    printf("%d年%d月有%d天\n", y, m, d);

    return 0;
}*/

/*⽆参数和⽆返回值函数在设计的时候，
⼀般情况下，函数是有参数和返回值的，
但是也有特殊情况，是不需要参数，也可能不需要返回值的
比如写⼀个简易的菜单，只是打印下⾯的内容：
****************************
    ****** 1. paly ******
    ****** 0. exit ******
*****************************/

/*#include <stdio.h>
void menu() // void 就是无，空的意思，表示这个函数无需传参
{
    printf("****************************\n");
    printf("****** 1. play ******\n");
    printf("****** 0. exit ******\n");
    printf("****************************\n");
}

int main()
{
    menu();
    meun(1);//无法传参
    meun(110); //

    return 0;
}*/

/*函数中的return语句*/
/*如果⼀个函数不需要返回值，在某种条件发⽣的时候，
想提前结束函数，也可以使⽤return语句*/

/*#include <stdio.h>

void cnm(int n)
{//输出1~n的值
    if (n < 1)
        return;//如果n<1就不用打印了，直接结束
    int i = 0;
    for (i = 1; i <= n; i++)
    {
        printf("%d ", i);
    }
}

int main()
{
    int n;
    scanf("%d", &n);
    cnm(n);

    return 0;
}*/

/*函数的声明和定义*/
// 可以函数调用，比如"eeee.h"

/*#include <stdio.h>
int is_leap_year(int y); // 函数声明
int main()
{
    int y = 0;
    scanf("%d", &y);
    int r = is_leap_year(y);
    if (r == 1)
        printf("闰年\n");
    else
        printf("⾮闰年\n");
    return 0;
}
// 判断⼀年是不是闰年
int is_leap_year(int y)//定义，也相当于声明了
{
    if (((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0))
        return 1;
    else
        return 0;
}*/

/*函数设计的原则
原则1：函数名的命名，参数的命名要有意义，能清晰的反应函数的实际功能和参数表达的意思，这样有利于代码的阅读和团队协作
原则2：函数的功能尽量独⽴、单⼀；这就是我们在软件⼯程中讲的⾼内聚，低耦合，这样有利于函数的复⽤
原则3：函数的参数尽量少，尽量不超过4个参数，参数过多使⽤难度就增加了，参数多就⼀定可以
继续拆分功能出来。
原则4：函数规模不要太⼤，规模太⼤，不⽅便维护；⼀般函数规模太⼤，就说明可以继续拆分。
原则5：函数中尽量不要使⽤全局变量，降低函数对外部变量的依赖
因为全局变量在函数外边也可能被别的代码使⽤，容易出错；还有就是全局变量的⽣命周期也⽐较
⻓，⽽函数可能是阶段性的调⽤。
原则6：如果函数不需要返回值，返回类型设置为void，如果不写，函数默认返回int类型的值。*/
/*#include <stdio.h>
test() // 默认是int类型，应该加个void
{
    printf("hehe\n");
}
int main()
{
    int n = test();
    printf("%d\n", n);
    return 0;
}*/

/*extern 关键字我们在写代码时候，经常会出现多个.c⽂件的情况，
有时候我们也需要调⽤来⾃外部源⽂件(.c)中的函数*/
/*#include <stdio.h>
// 声明来⾃外部的符号
// extern 关键字就是声明外部符号使⽤的
// extern 在说明Add函数是来⾃外部⽂件的，当前的源⽂件不包含这个函数的定义
extern int Add(int x, int y); // extern也是可以省略的
int main()
{
    int a = 10;
    int b = 20;
    int c = Add(a, b); // Add函数是定义在其他.c⽂件中的
    printf("%d\n", c);
    return 0;
}*/

/*！static
关键字C语⾔中⼀个常被考察的关键字是static的，这个关键字在C语⾔中3个作⽤。
• static修饰局部变量//改变了变量的存储类型，普通的局部变量是存储再栈区，static修饰后的局部变量存储在静态区
• static修饰全局变量//这是因为全局变量本来是具有外部链接属性的，⽽被static修饰之后，外部链接属性就变成了内部链接属性，
                     被static修饰的全局变量只能在⾃⼰所在的源⽂件内部使⽤，⽆法在其他源⽂件内部使⽤的。
                     所以，如果我们希望⼀个全局变量只能在⾃⼰所在的源⽂件内部使⽤，不让其他源⽂件使⽤，
                     就可以使⽤static修饰这个全局变量，达到这种效果。
• static修饰函数//static修饰函数和static修饰全局变量是⾮常类似的，本来函数是具有外部链接属性的，
                 在其他源⽂件内部声明后可以调⽤使⽤，但是被static修饰就变成了内部链接属性，
                 这时这个函数只能在⾃⼰所在的源⽂件内部使⽤，如果在其他源⽂件内部调⽤就报错；*/

/*局部变量*/
/*#include <stdio.h>
void test() // 局部变量进入函数创建，出了函数就销毁，
// n是局部变量，每次调用test函数都会新建一个n，赋值为5
{
    int n = 5; // 打印5个5，改成static int n = 5 就打印56789
               // 被static修饰的局部变量n只会被创建和初始化⼀次，不需要在每次调⽤test函数时都进⾏初始化。
               // 也就是说，在第⼆次调⽤test函数的时候，使⽤的是上次test函数结束时变量n的值。
               // 这样如果代码中对变量n进⾏调整，这种效果就会累积下来。所以看到了这样的输出。
    printf("%d ", n);
    n++;
}
int main()
{
    int i = 0;
    for (i = 0; i < 5; i++)
    {
        test();
    }
    return 0;
}*/

/*冒泡排序*/

/*#include <stdio.h>

void print_arr(int arr[], int sz)
{
    int i = 0;
    for (i = 0; i < sz; i++)
    {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

void bubble_sort(int arr[], int sz)
{
    // 趟数
    int i = 0;
    for (i = 0; i < sz - 1; i++)
    {
        // 一趟内部
        int flag = 1; // 假设已经有序了

        int j = 0;                       // j作为下标
        for (j = 0; j < sz - 1 - i; j++) // j<sz-1-i
        {
            if (arr[j] > arr[j + 1])
            {
                flag = 0; // 尚未有序
                int tmp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = tmp;
            }
        }
        if (flag == 1)
        {
            break;
        }
    }
}

int main()
{
    int arr[] = {9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
    int sz = sizeof(arr) / sizeof(arr[0]);
    print_arr(arr, sz);
    bubble_sort(arr, sz);
    print_arr(arr, sz);

    return 0;
}*/
/*#include <stdio.h>

void print_arr(int num[], int k)
{

    for (int i = 0; i < k; i++)
    {
        printf("%d ", num[i]);
    }
    printf("\n");
}

void bubble_sort(int arr[], int num[], int sz)
{
    // 趟数

    for (int i = 0; i < sz - 1; i++)
    {
        // 一趟内部
        int flag = 1; // 假设已经有序了

        // j作为下标
        for (int j = 0; j < sz - 1 - i; j++) // j<sz-1-i
        {
            if (arr[j] < arr[j + 1])

            {
                flag = 0; // 尚未有序
                int tmp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = tmp;

                int tmp2 = num[j];
                num[j] = num[j + 1];
                num[j + 1] = tmp2;
            }
        }
        if (flag == 1)
        {
            break;
        }
    }
}

int main()
{
    int arr[1000], n, k;
    int num[1000];
    scanf("%d%d", &n, &k);
    for (int l = 0; l < n; l++)
    {
        scanf("%d", &arr[l]);
        num[l] = l + 1;
    }
    bubble_sort(arr, num, n);
    print_arr(num, k);

    return 0;
}*/

/*内存与地址
 内存单元的编号 == 地址 == 指针1*
 cpu和和内存
读，cpu地址总线到内存，再通过数据总线传达信息到cpu
写，cou控制总线到内存，再通过地址总线
表示写到哪，最后通过数据总线传递信息到cpu*/

/*指针变量与地址*/
// 32位机器假设有32根地址总线，每根地址线出来的电信号转换成数字信号后
// 是1或者0，那我们把32根地址线产⽣的2进制序列当做⼀个地址，那么⼀个地址就是32个bit位，需要4个字节才能存储。
// 64位则是8个字节

/*#include <stdio.h>

int main()
{
    int a = 10;   // 向内存申请4个字节的空间，用来存放10
    int *pa = &a; // 取出a的地址并存储到指针变量pa中
    *pa = 20;     // 解引用操作（间接访问操作）
    printf("%d\n", a);
    // printf("%p\n", &a); //%p打印地址，&a取出的是a所占4个字节中地址较⼩的字节的地址。
    return 0;
}*/

/*• 32位平台下地址是32个bit位，指针变量⼤⼩是4个字节
• 64位平台下地址是64个bit位，指针变量⼤⼩是8个字节
• 注意指针变量的⼤⼩和类型是⽆关的，只要指针类型的变量，在相同的平台下，⼤⼩都是相同的。*/
/*#include <stdio.h>

int main()
{
    // int a = 10;
    // int *pa = &a;
    // printf("%d\n", sizeof(pa));//pa可以换成int*

    printf("%zd\n", sizeof(char *));
    printf("%zd\n", sizeof(short *));
    printf("%zd\n", sizeof(int *));
    printf("%zd\n", sizeof(double *));

    return 0;
}*/

/*// 代码1
#include <stdio.h>
int main()
{
    int n = 0x11223344;
    int *pi = &n;
    *pi = 0;
    return 0;
}

// 代码2
#include <stdio.h>
int main()
{
    int n = 0x11223344;
    char *pc = (char *)&n;
    *pc = 0;
    return 0;
}*/

/*调试我们可以看到，代码1会将n的4个字节全部改为0，但是代码2只是将n的第⼀个字节改为0。
    结论：指针的类型决定了，对指针解引⽤的时候有多⼤的权限（⼀次能操作⼏个字节）。
    ⽐如： char *的指针解引⽤就只能访问⼀个字节，⽽ int *的指针的解引⽤就能访问四个字节。*/

/*指针+-整数
指针的类型决定了，对指针解引⽤的时候有多⼤的权限（⼀次能操作⼏个字节）。
⽐如： char* 的指针解引⽤就只能访问⼀个字节，⽽ int* 的指针的解引⽤就能访问四个字节。*/

/*#include <stdio.h>
int main()
{
    int n = 10;
    char *pc = (char *)&n;//强制转换
    int *pi = &n;
    printf("%p\n", &n);
    printf("%p\n", pc);
    printf("%p\n", pc + 1);
    printf("%p\n", pi);
    printf("%p\n", pi + 1);
    return 0;
}*/

/*void* 指针在指针类型中有⼀种特殊的类型是 void* 类型的，
可以理解为⽆具体类型的指针（或者叫泛型指针），
这种类型的指针可以⽤来接受任意类型地址。但是也有局限性，
 void* 类型的指针不能直接进⾏指针的+-整数和解引⽤的运算。*/

/*#include <stdio.h>
int main()
{
    int a = 10;
    int *pa = &a;
    char *pc = &a;//不兼容
    return 0;
}*/

/*#include <stdio.h>
int main()
{
    int a = 10;
    int *pa = &a;
    void *pc = &a;

    double d = 3.14;
    void *pd = &d;
    //*pd=8.5//error
    pd = pd + 1; // error

    // void *可以⽤来接受！任意类型地址,用的时候再转换成想要的类型
    // ⼀般 void* 类型的指针是使⽤在函数参数的部分，⽤来接收不同类型数据的地址，
    //这样的设计可以实现泛型编程的效果，使得⼀个函数来处理多种类型的数据。

    return 0;
}*/

/*const修饰指针
 ⼀个变量加上⼀些限制，不能被修改，这就是const的作⽤。*/

/*#include <stdio.h>
int main()
{
    int m = 0;
    m = 20; // m是可以修改的
    const int n = 0;//常变量
    n = 20; // n是不能被修改的
    return 0;
}*/

/*#include <stdio.h>

int main()
{
    const int n = 10;

    int *p = &n;
    *p = 20;//利用n的地址来修改

    printf("%d\n", n);

    return 0;
}*/

/*？结论：左内右地址变量
const修饰指针变量的时候
• const如果放在*的左边，修饰的是指针指向的内容，
保证指针指向的内容不能通过指针来改变。但是指针变量本⾝的内容可变。
• const如果放在*的右边，修饰的是指针变量本⾝，
保证了指针变量的内容不能修改，但是指针指向的内容，可以通过指针改变。*/

/*#include <stdio.h>
// 代码1 - 测试⽆const修饰的情况
void test1()
{
    int n = 10;
    int m = 20;
    int *p = &n; // p没有被const修饰，p和*p都可以修改
    *p = 20;     // yes
    p = &m;      // yes
}
// 代码2 - 测试const放在*的左边情况
void test2()
{
    int n = 10;
    int m = 20;
    const int *p = &n; // 或者int const
    // 限制的是指针变量指向的对象，也就是说不能通过指针变量修改指针指向的内容
    // 但是可以修改指针变量本身的值，也就是修改指针变量的指向
    *p = 20; // no
    p = &m;  // yes
}
// 代码3 - 测试const放在*的右边情况
void test3()
{
    int n = 10;
    int m = 20;
    int *const p = &n;
    // 修饰的是指针变量本身，限制了指针变量的指向，也就是指针变量不能调整指向
    // 不限制指针指向的内容，也就是通过指针变量修改指针指向的内容是可以的
    *p = 20; // yes
    p = &m;  // no
}
// 代码4 - 测试*的左右两边都有const
void test4()
{
    int n = 10;
    int m = 20;
    int const *const p = &n;
    *p = 20; // no
    p = &m;  // no
}
int main()
{
    // 测试⽆const修饰的情况
    test1();
    // 测试const放在*的左边情况
    test2();
    // 测试const放在*的右边情况
    test3();
    // 测试*的左右两边都有const
    test4();
    return 0;
}*/

/*指针运算
指针的基本运算有三种，分别是：
• 指针+- 整数
• 指针-指针
• 指针的关系运算*/

/*1.1 指针 +- 整数因为数组在内存中是连续存放的，
只要知道第⼀个元素的地址，顺藤摸⽠就能找到后⾯的所有元素。*/

/*#include <stdio.h>

int main()
{
    int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int *p = &arr[0];
    int sz = sizeof(arr) / sizeof(arr[0]);
    for (int i = 0; i < sz; i++)

    {
        printf("%d ", *(p + i));
    }
    for (int i = 0; i < sz, i++)
    {
        printf("%p==%p\n", &arr[i], p + i);
    }

return 0;
}
*/

/*1.2 指针-指针
就像⽇期-⽇期得到天数⼀样，指针和指针可以相减，
指针-指针的绝对值是指针和指针之间元素的个数。
指针-指针的前提是两个指针指向同⼀块空间（⽐如同⼀个数组）。*/

/*#include <stdio.h>

int main()
{
    int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    printf("%d\n", &arr[9] - &arr[0]);//9
    printf("%d\n", &arr[0] - &arr[9]);//-9

    return 0;
}*/

/*应用
strlen*/

/*#include <stdio.h>
#include <string.h>

int my_strlen(char *str)
{
    char *start = str;
    while (*str != '\0')
    {
        str++;
    }
    return str - start;
}

int main()
{
    char arr[] = "abcdef";
    // a b c d e f \0
    // strlen统计的是字符串中\0之前的个数
    // 数组的数组名是数组首元素的地址
    // arr ==> &arr[0]

    int len = my_strlen(arr);

    printf("%d\n", len);

    return 0;
}*/

/*指针的关系运算*/

/*#include <stdio.h>
int main()
{
    int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int *p = &arr[0];
    int i = 0;
    int sz = sizeof(arr) / sizeof(arr[0]);
    while (p < arr + sz) // 指针的⼤⼩⽐较,或者写成p<=&arr[sz-1],p<&arr[sz]
    {
        printf("%d ", *p);
        p++;
    }
    return 0;
}*/

/*野指针
 野指针就是指针指向的位置是不可知的（随机的、不正确的、没有明确限制的）
 理解野指针的时候，你可以把野指针想成“野狗”，是没有主⼈的，是危险的。*/

/*1.1 野指针成因1. 指针未初始化*/

/*1. 指针未初始化*/
/*#include <stdio.h> //不初始化，是随机值

int main()
{
    int m;
    int *p;
    printf("%d\n", m);
    printf("%p\n", p);

    return 0;
}*/

/*#include <stdio.h>
int main()
{

    int *p;  // 局部变量指针未初始化，默认为随机值
    *p = 20; // err
    printf("%p", p);
    return 0;
}*/
/*#include <stdio.h>
int main()
{
    int arr[1000] = {};
    int *p = &arr[0];
    // 局部变量指针未初始化，默认为随机值
    *p = 20; // err
    printf("%p", p);
    return 0;
}*/

/*2. 指针越界访问*/

/*#include <stdio.h>

int main()
{
    int arr[10] = {0};
    int *p = &arr[0];
    for (int i = 0; i <= 11; i++)
    {               // 当指针指向的范围超出数组arr的范围时，p就是野指针
        *(p++) = i; // 后置++，先使用再++
    }
}*/

/*指针指向的空间释放*/

/*#include <stdio.h>

int *test()
{
    int n = 100;
    return &n;
}

int main()
{
    int *p = test();
    printf("%d\n", *p);
    return 0;
}*/

/*在printf函数中，当我们想要输出指针p所指向的值时，使用*p。
例如，printf("%d", *p);将输出p指向的地址处的值。
如果我们想要输出指针p本身，即它所存储的地址，使用%p格式说明符，
例如printf("%p", p);将输出指针p的地址。*/

/*1.2 如何规避野指针*/

/*1.2.1 指针初始化
如果明确知道指针指向哪⾥就直接赋值地址，
如果不知道指针应该指向哪⾥，可以给指针赋值NULL.*/

/*#include <stdio.h>
int main()
{
    int num = 10;
    int *p1 = &num;
    int *p2 = NULL; // 不知道指向哪
    return 0;
}*/

/*1.2.2 ⼩⼼指针越界
⼀个程序向内存申请了哪些空间，通过指针也就只能访问哪些空间，
不能超出范围访问，超出了就是越界访问。
使⽤指针的时候⼀定要注意边界，通过指针访问的内存是不能越界的。*/

/*#include <stdio.h>
int main()
{
    int arr[10] = {0};
    int *p = arr;
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        *p = 1;
        p++; // 当循环到第11次的时候，指针就越界了,越界了但没有访问
    }
    return 0;
}*/

/*1.2.3 指针变量不再使⽤时，及时置NULL，指针使⽤之前检查有效性
当指针变量指向⼀块区域的时候，我们可以通过指针访问该区域，
后期不再使⽤这个指针访问空间的时候，
我们可以把该指针置为NULL。因为约定俗成的⼀个规则就是：
只要是NULL指针就不去访问，同时使⽤指针之前可以判断指针是否为NULL。*/

/*#include <stdio.h>

int main()
{
    int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int *p = &arr[0];
    for (int i = 0; i < 10; i++)
    {
        *(p++) = i;
    }
    // 此时p已经越界了，可以把p置为NULL
    p = NULL;
    // 下次使⽤的时候，判断p不为NULL的时候再使⽤
    //...
    p = &arr[0];   // 重新让p获得地址
    if (p != NULL) // 判断
    {
        //...
    }
    return 0;
}*/

/*assert断⾔
1. assert 断⾔
assert.h 头⽂件定义了宏 assert() ，⽤于在运⾏时确保程序符合指定条件，
如果不符合，就报错终⽌运⾏。这个宏常常被称为“断⾔”。
assert() 宏接受⼀个表达式作为参数。
• 如果该表达式为真（返回值⾮零）， assert() 不会产⽣任何作⽤，程序继续运⾏。
• 如果该表达式为假（返回值为零）， assert() 就会报错，
在标准错误流 stderr 中写⼊⼀条错误信息，显⽰没有通过的表达式，
以及包含这个表达式的⽂件名和⾏号。

2. assert的好处
assert() 的使⽤对程序员是⾮常友好的，使⽤ assert() 有⼏个好处：
它不仅能⾃动标识⽂件和出问题的⾏号，还有⼀种⽆需更改代码就能开启或关闭 assert() 的机制。
如果已经确认程序没有问题，不需要再做断⾔，就在 #include <assert.h> 语句的前⾯，定义⼀
个宏 NDEBUG 。
#define NDEBUG
#include <assert.h>
然后，重新编译程序，编译器就会禁⽤⽂件中所有的 assert() 语句。
如果程序⼜出现问题，可以移除这条 #define NDBUG 指令（或者把它注释掉），
再次编译，这样就重新启⽤了 assert() 语句。
assert() 的缺点是，因为引⼊了额外的检查，增加了程序的运⾏时间。
⼀般我们可以在 Debug 中使⽤，在 Release 版本中选择禁⽤ assert 就⾏，在 VS 这样的集成开
发环境中，在 Release 版本中，直接就是优化掉了。这样在debug版本写有利于程序员排查问题，
在 Release 版本不影响⽤⼾使⽤时程序的效率。*/
